Skip to content

refactor: extract shared request handling from server entry points#258

Open
yunus25jmi1 wants to merge 6 commits intocloudflare:mainfrom
yunus25jmi1:refactor/shared-request-handling
Open

refactor: extract shared request handling from server entry points#258
yunus25jmi1 wants to merge 6 commits intocloudflare:mainfrom
yunus25jmi1:refactor/shared-request-handling

Conversation

@yunus25jmi1
Copy link
Contributor

@yunus25jmi1 yunus25jmi1 commented Mar 5, 2026

Summary

Deduplicates ~250 lines of inline request-handling logic from the generated App Router RSC entry (app-dev-server.ts) and Pages Router server entry (index.ts) by importing from the canonical config-matchers.ts and a new request-pipeline.ts module.

Changes

New file: server/request-pipeline.ts

Shared request lifecycle utilities extracted for reuse across entry points:

  • guardProtocolRelativeUrl() — blocks //evil.com protocol-relative URL attacks
  • stripBasePath() — removes configured basePath prefix from pathnames
  • normalizeTrailingSlash() — redirects to canonical trailing-slash form
  • validateCsrfOrigin() — CSRF origin validation for server actions
  • isOriginAllowed() — wildcard subdomain matching for allowed origins
  • validateImageUrl() — image optimization URL validation
  • processMiddlewareHeaders() — strips internal x-middleware-* headers

app-dev-server.ts (~250 lines removed)

Replaced 14 inline __-prefixed functions with imports from config-matchers.ts and request-pipeline.ts:

  • Config matching: matchRedirect, matchRewrite, matchHeaders, sanitizeDestination
  • External proxying: isExternalUrl, proxyExternalRequest
  • Request context: requestContextFromRequest
  • Security: validateCsrfOrigin, validateImageUrl

index.ts

  • Replaced inline parseCookies in generated server entry template with import from config-matchers.ts
  • Replaced manual RequestContext construction with requestContextFromRequest()
  • Removed duplicate sanitizeDestinationLocal(), using imported sanitizeDestination()
  • Removed duplicate matchConfigPattern + extractConstraint (~100 lines), re-exporting from config-matchers.ts

Test updates

  • Updated app-router.test.ts assertions to reference the new imported function names instead of old inline __-prefixed names.
  • Added request-pipeline.test.ts with 33 unit tests covering all 6 exported functions.

Behavioral Change

normalizeTrailingSlash now uses relative Location headers instead of absolute URLs. The old inline code used Response.redirect(new URL(..., request.url), 308) which produces an absolute Location header; the new code uses new Response(null, { status: 308, headers: { Location: relativePath } }). This is valid per RFC 7231 §7.1.2 and aligns dev behavior with prod-server.ts (which already uses relative Location headers).

Security Fixes

  • matchConfigPattern: eliminates chained .replace() divergence (CodeQL flagged incomplete sanitization) in favor of config-matchers single-pass tokenizer
  • proxyExternalRequest: uses AbortController+setTimeout (broader runtime support) instead of AbortSignal.timeout()
  • escapeHeaderSource: uses proper config-matchers implementation instead of simplified chained .replace()

Verification

  • Typecheck: pnpm run typecheck passes clean
  • Shims tests: 575/575 passed
  • App Router tests: 209/209 passed
  • Request pipeline tests: 33/33 passed

Closes #254

@pkg-pr-new
Copy link

pkg-pr-new bot commented Mar 5, 2026

Open in StackBlitz

npm i https://pkg.pr.new/vinext@258

commit: 011e8b4

@yunus25jmi1 yunus25jmi1 force-pushed the refactor/shared-request-handling branch from 14ccb24 to 631e3fc Compare March 5, 2026 02:19
@yunus25jmi1
Copy link
Contributor Author

yunus25jmi1 commented Mar 5, 2026

All checks have passed with all green. Kindly review the changes. @southpolesteve @elithrar

@yunus25jmi1 yunus25jmi1 force-pushed the refactor/shared-request-handling branch 2 times, most recently from 2cb7c43 to 6624adc Compare March 8, 2026 08:10
@yunus25jmi1
Copy link
Contributor Author

@southpolesteve review the changes. Conflicts resolved.

@james-elicx
Copy link
Collaborator

/bigbonk review

Copy link
Contributor

@ask-bonk ask-bonk bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: Extract shared request handling from server entry points

The direction of this PR is right — deduplicating the inline __-prefixed helpers from the generated code into importable modules is a meaningful improvement. Having a single canonical matchConfigPattern (single-pass tokenizer) instead of the chained .replace() variant is a real fix.

However, there are several issues that need to be addressed before this can merge.

Key Issues

  1. Dead code in request-pipeline.ts — Four of the seven exported functions (guardProtocolRelativeUrl, stripBasePath, normalizeTrailingSlash, processMiddlewareHeaders) are never imported or called anywhere in the codebase. The protocol-relative guard is still inlined in app-dev-server.ts at line 1125. This new module ships dead code that increases the bundle without any deduplication benefit. Either wire up these functions at the call sites that have the inline versions, or remove them from this PR and add them when they're actually used.

  2. Behavioral difference in validateImageUrl — The old code resolved the image URL against request.url (full URL with path+query), while the new code resolves against url.origin (scheme+host only). For root-relative paths like /images/foo.png this is equivalent, but the origin check inside validateImageUrl resolves against url.origin too (line 190), which means the defense-in-depth check is comparing url.origin === url.origin — it's a tautology. This was also a tautology in the original code (since /images/foo.png resolved against the same origin will always keep the same origin), so the security posture is unchanged, but it would be good to document why this defense-in-depth check exists despite being tautological for the filtered inputs.

  3. Extra blank lines in generated code — The removal of the inline functions left orphaned blank lines in the generated entry at two locations (lines 1049-1051 and 1173-1176). Minor, but noticeable in the generated output.

  4. safeRegExp removed from imports but was previously used in index.ts — The diff removes safeRegExp from the config-matchers.js import in index.ts (line 29-38). Verify this isn't used elsewhere in the file or re-exported for consumers.

Correctness Observations

  • The validateCsrfOrigin signature change from closure-captured __allowedOrigins to an explicit parameter is clean and correct. The call site at line 1352 passes __allowedOrigins explicitly.
  • The matchRedirect/matchRewrite/matchHeaders functions in config-matchers.ts take individual rule arrays, which matches how they're called in the generated code (e.g., matchRedirect(__redirPathname, __configRedirects, __reqCtx)).
  • The requestContextFromRequest replacement for __buildRequestContext is a straightforward 1:1 swap with the same semantics.
  • The matchConfigPattern re-export from index.ts (line 3749) preserves the public API for external consumers.
  • Test updates correctly reflect the new function names and are otherwise semantically equivalent.

Suggestions

See inline comments.

return new Response("404 Not Found", { status: 404 });
}
return null;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

guardProtocolRelativeUrl is exported but never imported anywhere. The protocol-relative URL guard is still inlined in app-dev-server.ts at line 1125:

if (url.pathname.replaceAll("\\\\" , "/").startsWith("//")) {

Either replace the inline check with this function, or remove it from this PR. Shipping dead code in a module intended to deduplicate code is counterproductive.

return pathname.slice(basePath.length) || "/";
}
return pathname;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

stripBasePath is exported but never imported anywhere in the codebase. The inline pathname.slice(basePath.length) || "/" pattern is still used directly in app-dev-server.ts (line 1141) and index.ts. Same concern as guardProtocolRelativeUrl — either wire it up or defer to a follow-up PR.

});
}
return null;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

normalizeTrailingSlash is exported but never called. The trailing slash logic is still inlined in app-dev-server.ts at lines 1146-1153. Same issue.

for (const key of keysToDelete) {
headers.delete(key);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

processMiddlewareHeaders is exported but never imported. The middleware header unpacking is still done inline in app-dev-server.ts at lines 1260-1267. This is the fourth unused function in this module.

To summarize: only validateCsrfOrigin and validateImageUrl from this file are actually wired up. The other four functions are dead code.

}
}


Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stray extra blank lines left behind from removing the inline functions. Two consecutive blank lines here; please collapse to one.

Suggested change

});
return new Response(upstream.body, { status: upstream.status, statusText: upstream.statusText, headers: respHeaders });
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue — two consecutive blank lines left over from the removal of __proxyExternalRequest and __applyConfigHeaders. Please collapse.

Suggested change

Comment on lines +188 to +194
// Validate the constructed URL's origin hasn't changed (defense in depth).
const url = new URL(requestUrl);
const resolvedImg = new URL(imgUrl, url.origin);
if (resolvedImg.origin !== url.origin) {
return new Response("Only relative URLs allowed", { status: 400 });
}
return imgUrl;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The defense-in-depth origin check is tautological for all inputs that pass the guard above (must start with / but not //). Resolving a root-relative path against url.origin will always produce the same origin. This was also true in the original inline code, but since this is now a standalone function with its own documentation, it would be helpful to add a comment explaining that this check exists as a safety net against future changes to the guard logic, not because it catches anything today.

@ask-bonk
Copy link
Contributor

ask-bonk bot commented Mar 8, 2026

Review submitted with REQUEST_CHANGES status. Here's a summary of the key findings:

The PR's direction is sound — deduplicating ~250 lines of inline __-prefixed helpers into shared modules is a real improvement, especially replacing the chained .replace() matchConfigPattern with the canonical single-pass tokenizer from config-matchers.ts.

Issues flagged:

  1. Dead code — 4 of 7 functions in the new request-pipeline.ts (guardProtocolRelativeUrl, stripBasePath, normalizeTrailingSlash, processMiddlewareHeaders) are never imported anywhere. The inline versions still exist in the generated code. These should either be wired up or deferred.

  2. validateImageUrl origin check is a tautology for filtered inputs (always same-origin after the /-but-not-// guard). Not a regression, but worth documenting since it's now a standalone function.

  3. Stray double blank lines left behind at two locations in the generated code.

  4. safeRegExp removed from imports in index.ts — needs verification it's not used elsewhere in that file.

Correctness is solid for the parts that are actually wired up: validateCsrfOrigin, validateImageUrl, matchRedirect/matchRewrite/matchHeaders, requestContextFromRequest, sanitizeDestination, and matchConfigPattern re-export all have correct signatures and call sites.

github run

@yunus25jmi1 yunus25jmi1 force-pushed the refactor/shared-request-handling branch from 6624adc to 439ebd3 Compare March 8, 2026 10:25
@yunus25jmi1
Copy link
Contributor Author

@james-elicx Re-review the changes. Kindly merge the PR.

@james-elicx
Copy link
Collaborator

/bigbonk re-review this pr

Copy link
Contributor

@ask-bonk ask-bonk bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: Extract shared request handling from server entry points

The direction is right — deduplicating inline __-prefixed helpers into importable modules is a clear improvement, and replacing the chained .replace() matchConfigPattern with the canonical single-pass tokenizer from config-matchers.ts is a real security fix. The safeRegExp removal is clean, the matchConfigPattern re-export preserves the public API correctly, and the test updates are mechanically sound.

However, there are correctness issues and misleading documentation that need to be addressed before this can merge.

Issues

1. normalizeTrailingSlash changes redirect behavior from absolute to relative URLs

The old code used Response.redirect(new URL(path, request.url), 308) which produces an absolute Location header. The new code uses new Response(null, { status: 308, headers: { Location: relativePath } }) which produces a relative path. This is not a regression — relative Location headers are valid per RFC 7231, and prod-server.ts already uses relative paths (line 769). So this actually brings dev/prod into alignment. However, it's a deliberate behavioral change that should be called out in the PR description rather than hidden.

2. processMiddlewareHeaders does more than the code it replaced

The old inline code only deleted x-middleware-* headers. The new processMiddlewareHeaders also has logic to unpack x-middleware-request-* into an applyTo target. At the call site (line 1245), applyTo is not passed, so the extra logic is dead. This isn't a bug, but the function's documented behavior ("copies x-middleware-request-* values to actual header names") is misleading at this call site since applyMiddlewareRequestHeaders() on line 1244 already handles that unpacking. The applyTo parameter suggests this function should be a complete replacement for applyMiddlewareRequestHeaders + the delete loop, but it isn't actually used that way.

3. Module header comment is inaccurate

The request-pipeline.ts module comment (lines 4-6) claims these utilities were "extracted from the four server entry points (app-dev-server.ts, dev-server.ts, prod-server.ts, index.ts configureServer)." In reality, only app-dev-server.ts uses them. dev-server.ts, prod-server.ts, and index.ts configureServer still have their own inline implementations. This should either say "extracted from app-dev-server.ts" or the other files should be updated to use these shared functions too (which is presumably the intent for a follow-up).

4. No unit tests for the new request-pipeline.ts functions

The existing tests only verify that the generated code string contains the right function names. There are no unit tests for the actual runtime behavior of guardProtocolRelativeUrl, stripBasePath, normalizeTrailingSlash, validateCsrfOrigin, validateImageUrl, or processMiddlewareHeaders. Since these functions are now standalone importable modules (not inlined in generated code), they should have unit tests that verify their behavior directly — especially the security-sensitive ones like CSRF validation and image URL validation.

*
* Extracted from the four server entry points (app-dev-server.ts,
* dev-server.ts, prod-server.ts, index.ts configureServer) to ensure
* consistent behavior across dev and production, Pages Router and App Router.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment is inaccurate. Only app-dev-server.ts imports from this module. dev-server.ts, prod-server.ts, and index.ts configureServer still have their own inline implementations of these functions. Either update the comment to reflect reality ("extracted from app-dev-server.ts") or wire up the other entry points in this PR.

Suggested change
* consistent behavior across dev and production, Pages Router and App Router.
* Extracted from the App Router RSC entry (app-dev-server.ts) to enable
* reuse across entry points. Currently consumed by app-dev-server.ts;
* dev-server.ts, prod-server.ts, and index.ts still have inline versions
* that should be migrated in follow-up work.

if (trailingSlash && !hasTrailing && !pathname.endsWith(".rsc")) {
return new Response(null, {
status: 308,
headers: { Location: basePath + pathname + "/" + search },
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a subtle behavioral change from the old code. The old inline code used Response.redirect(new URL(basePath + pathname + "/" + search, request.url), 308) which produces an absolute Location URL. This new code produces a relative path.

This is actually an improvement — it aligns dev behavior with prod-server.ts (line 769) which already uses relative Location headers, and relative redirects are valid per RFC 7231. But it should be documented in the PR description since it changes what browsers see in the Location header.

* @param headers - The Headers object to modify in place
* @param applyTo - Optional target Headers to receive the unpacked values
*/
export function processMiddlewareHeaders(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The applyTo parameter is never used at the call site (line 1245 of app-dev-server.ts). The function's JSDoc says it "copies x-middleware-request-* values to the actual header names" but at the call site, applyMiddlewareRequestHeaders() (called on line 1244) already handles that unpacking. So this function only acts as a delete loop.

This makes the function signature misleading — it looks like it should be a complete replacement for the two-step applyMiddlewareRequestHeaders + delete pattern, but it isn't used that way. Either:

  1. Remove the applyTo parameter and simplify to just the delete logic (matching what the call site needs), or
  2. Actually use this as the complete replacement (removing the applyMiddlewareRequestHeaders call at the call site)

Option 1 is safer for this PR.

});
}
return null;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function doesn't have access to request.url (it only receives pathname, basePath, trailingSlash, search), so it can't construct absolute redirect URLs. This is fine — relative Location is correct — but it means callers can't opt into absolute URLs if they need to. Worth noting in the JSDoc that the returned Response uses a relative Location header.

${middlewarePath ? `import * as middlewareModule from ${JSON.stringify(middlewarePath.replace(/\\/g, "/"))};` : ""}
${instrumentationPath ? `import * as _instrumentation from ${JSON.stringify(instrumentationPath.replace(/\\/g, "/"))};` : ""}
${effectiveMetaRoutes.length > 0 ? `import { sitemapToXml, robotsToText, manifestToJson } from ${JSON.stringify(fileURLToPath(new URL("./metadata-routes.js", import.meta.url)).replace(/\\/g, "/"))};` : ""}
import { requestContextFromRequest, matchRedirect, matchRewrite, matchHeaders, isExternalUrl, proxyExternalRequest, sanitizeDestination } from ${JSON.stringify(configMatchersPath)};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This import pulls in guardProtocolRelativeUrl, stripBasePath, normalizeTrailingSlash, and processMiddlewareHeaders from request-pipeline.ts. The previous review flagged these as dead code, but they're actually used — they appear in the generated code template below (lines 1117, 1131, 1136, 1245). The confusion arose because they're inside a template literal, not a regular import.

That said, stripBasePath is only emitted when basePath is configured (gated by ${bp ? ...}), and processMiddlewareHeaders is only emitted when middleware exists. The import is unconditional though, which means these functions are imported even when not needed. This is a minor bundle concern for dev-only code — not blocking, but worth noting.

@ask-bonk
Copy link
Contributor

ask-bonk bot commented Mar 8, 2026

Review submitted successfully. Here's a summary of what I flagged:

Review Summary

Overall assessment: REQUEST_CHANGES — The deduplication direction is sound, but there are documentation accuracy issues and missing test coverage that should be addressed.

Key findings:

  1. Module header comment is inaccuraterequest-pipeline.ts claims extraction from four server entry points, but only app-dev-server.ts actually uses it. The other three files still have inline implementations.

  2. normalizeTrailingSlash changes redirect behavior — Switches from absolute to relative Location headers. Not a regression (actually aligns dev with prod-server behavior), but should be documented in the PR description.

  3. processMiddlewareHeaders has unused applyTo parameter — The function documents header unpacking behavior that's dead at the call site (since applyMiddlewareRequestHeaders already handles it). The parameter should be removed or the function should fully replace both steps.

  4. No unit tests for new standalone functions — Security-sensitive functions like validateCsrfOrigin and validateImageUrl are now importable modules but have zero direct unit tests. The existing tests only check generated code strings contain the right function names.

  5. Previous reviewer's "dead code" concern was incorrect — The functions flagged as unused are actually called inside the generated code template literal in app-dev-server.ts. The safeRegExp removal is clean and correct.

github run

…quest-pipeline

Deduplicate ~250 lines of inline request-handling logic from the generated
App Router RSC entry (app-dev-server.ts) and Pages Router server entry
(index.ts) by importing from the canonical config-matchers.ts and a new
request-pipeline.ts module.

Changes:
- Create server/request-pipeline.ts with shared utilities:
  guardProtocolRelativeUrl, stripBasePath, normalizeTrailingSlash,
  validateCsrfOrigin, isOriginAllowed, validateImageUrl,
  processMiddlewareHeaders
- app-dev-server.ts: replace 14 inline __-prefixed functions with imports
  from config-matchers.ts (matchRedirect, matchRewrite, matchHeaders,
  isExternalUrl, proxyExternalRequest, sanitizeDestination,
  requestContextFromRequest) and request-pipeline.ts (validateCsrfOrigin,
  validateImageUrl)
- index.ts: replace inline parseCookies, sanitizeDestinationLocal, and
  matchConfigPattern with imports/re-exports from config-matchers.ts;
  replace manual RequestContext construction with requestContextFromRequest()
- Update tests to reference imported function names instead of old inline
  __-prefixed names

Security fixes:
- matchConfigPattern: eliminates chained .replace() divergence (CodeQL
  flagged) in favor of config-matchers single-pass tokenizer
- proxyExternalRequest: uses AbortController+setTimeout (broader runtime
  support) instead of AbortSignal.timeout()
- escapeHeaderSource: uses proper config-matchers implementation instead of
  simplified chained .replace()

Closes cloudflare#254

t status
@yunus25jmi1 yunus25jmi1 force-pushed the refactor/shared-request-handling branch from 439ebd3 to b59724d Compare March 8, 2026 11:40
@james-elicx
Copy link
Collaborator

/bigbonk review this pr please

Copy link
Contributor

@ask-bonk ask-bonk bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: Extract shared request handling from server entry points

The direction of this PR is right — deduplicating the inline __-prefixed helpers into importable modules is a clear improvement. Replacing the chained .replace() matchConfigPattern with the canonical single-pass tokenizer from config-matchers.ts is a real fix. The module header comment accurately reflects the current state (after the second reviewer's suggestion was adopted). The new request-pipeline.test.ts test file is a meaningful addition.

However, there are a few issues that should be addressed.

Key Issues

  1. processMiddlewareHeaders has a misleading applyTo parameter that is dead at its only call site — The function documents unpacking x-middleware-request-* headers into a target, but the call site (line 1245) never passes applyTo. The JSDoc says it "copies x-middleware-request-* values to actual header names" but applyMiddlewareRequestHeaders() on line 1244 already handles that. The tests don't cover the applyTo path either. This parameter should be removed to match actual usage.

  2. Dangling comment in index.ts — Line 3751 (// sanitizeDestination is imported from config-matchers.ts) is a stub comment that doesn't correspond to any code. It's not a re-export, not an import — just a leftover from removing sanitizeDestinationLocal. Should be removed.

  3. normalizeTrailingSlash changes redirect behavior — The old code used Response.redirect(new URL(..., request.url)) producing an absolute Location header. The new code returns new Response(null, { status: 308, headers: { Location: relativePath } }) producing a relative path. This is actually an improvement (aligns dev with prod-server.ts, and relative Location is valid per RFC 7231), but it's a deliberate behavioral change that should be noted in the PR description.

Minor

  1. The processMiddlewareHeaders tests only cover the no-applyTo code path. If applyTo is kept, it needs test coverage.

What looks good

  • The matchConfigPattern re-export from index.ts preserves the public API correctly
  • sanitizeDestinationLocalsanitizeDestination swap is clean and correct
  • requestContextFromRequest usage in both index.ts and app-dev-server.ts is a straightforward 1:1 replacement
  • validateCsrfOrigin signature change from closure-captured __allowedOrigins to an explicit parameter is clean
  • The test file exercises all 6 exported functions with reasonable coverage
  • The module header comment is now accurate (after incorporating the second bot review's suggestion)

* @param headers - The Headers object to modify in place
* @param applyTo - Optional target Headers to receive the unpacked values
*/
export function processMiddlewareHeaders(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The applyTo parameter is never passed at the only call site (app-dev-server.ts:1245). The function's JSDoc says it "copies x-middleware-request-* values to the actual header names" but this step is already handled by applyMiddlewareRequestHeaders() (called on line 1244, imported from next/headers). So at the call site, this function only acts as a delete loop.

The applyTo parameter and its associated logic (lines 224-225) are dead code. The test file also doesn't cover this path.

Suggestion: remove applyTo and simplify. If you intend this function to eventually replace applyMiddlewareRequestHeaders entirely, that should happen in this PR (replacing the two-step pattern) rather than shipping dead code.

Suggested change
export function processMiddlewareHeaders(
export function processMiddlewareHeaders(
headers: Headers,
): void {

Comment on lines +218 to +226
const prefix = "x-middleware-request-";
const keysToDelete: string[] = [];

for (const key of headers.keys()) {
if (key.startsWith(prefix)) {
const realName = key.slice(prefix.length);
if (applyTo) {
applyTo.set(realName, headers.get(key)!);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If applyTo is removed per the comment above, this block simplifies to just collecting keys to delete:

Suggested change
const prefix = "x-middleware-request-";
const keysToDelete: string[] = [];
for (const key of headers.keys()) {
if (key.startsWith(prefix)) {
const realName = key.slice(prefix.length);
if (applyTo) {
applyTo.set(realName, headers.get(key)!);
}
const keysToDelete: string[] = [];
for (const key of headers.keys()) {
if (key.startsWith("x-middleware-")) {
keysToDelete.push(key);
}
}

// divergence that CodeQL flagged as incomplete sanitization).
export { matchConfigPattern } from "./config/config-matchers.js";

// sanitizeDestination is imported from config-matchers.ts
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment is a dangling stub — it doesn't correspond to any code. sanitizeDestination is imported at line 36 and used in applyRedirects/applyRewrites below. The comment was presumably left behind when sanitizeDestinationLocal was removed. Remove it.

Suggested change
// sanitizeDestination is imported from config-matchers.ts

if (trailingSlash && !hasTrailing && !pathname.endsWith(".rsc")) {
return new Response(null, {
status: 308,
headers: { Location: basePath + pathname + "/" + search },
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a subtle behavioral change from the old code. The old inline code used Response.redirect(new URL(basePath + pathname + "/" + search, request.url), 308) which produces an absolute Location URL. This new code produces a relative path.

This is actually an improvement — it aligns dev behavior with prod-server.ts (which already uses relative Location headers), and relative redirects are valid per RFC 7231. But it's a behavioral change that should be documented in the PR description since it changes what browsers see in the Location header.

expect(headers.get("content-type")).toBe("text/html");
expect(headers.get("x-custom")).toBe("keep");
});
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing test coverage for the applyTo parameter of processMiddlewareHeaders. If that parameter is kept (see other comment), there should be a test like:

it("unpacks x-middleware-request-* headers into applyTo target", () => {
  const headers = new Headers({
    "x-middleware-request-x-custom": "value",
    "x-middleware-next": "1",
  });
  const target = new Headers();
  processMiddlewareHeaders(headers, target);
  expect(target.get("x-custom")).toBe("value");
  expect(headers.has("x-middleware-request-x-custom")).toBe(false);
  expect(headers.has("x-middleware-next")).toBe(false);
});

Or remove the parameter entirely (preferred — see review comment on request-pipeline.ts:214).

@ask-bonk
Copy link
Contributor

ask-bonk bot commented Mar 8, 2026

Review submitted successfully. Here's a summary of what I flagged:

REQUEST_CHANGES with 5 inline comments:

  1. processMiddlewareHeaders dead applyTo parameter — The parameter is never used at the only call site, where applyMiddlewareRequestHeaders() already handles the unpacking. Suggested removing it and simplifying the function to just a delete loop.

  2. Simplification of the loop body — If applyTo is removed, the inner loop no longer needs the x-middleware-request-* special case and can just collect all x-middleware-* keys for deletion.

  3. Dangling comment in index.ts:3751// sanitizeDestination is imported from config-matchers.ts is a leftover stub from removing sanitizeDestinationLocal. Suggested deleting it.

  4. normalizeTrailingSlash behavioral change — Switches from absolute to relative Location headers. This is actually an improvement (aligns dev with prod), but should be documented in the PR description.

  5. Missing test coverage — The processMiddlewareHeaders tests don't exercise the applyTo code path. Either add a test or remove the parameter.

The overall deduplication direction is sound — the matchConfigPattern consolidation, requestContextFromRequest swap, validateCsrfOrigin parameterization, and test updates are all correct.

github run

@james-elicx
Copy link
Collaborator

/bigbonk review this pr please

Copy link
Contributor

@ask-bonk ask-bonk bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: Extract shared request handling from server entry points

The direction is right — consolidating inline __-prefixed helpers into importable modules is a clear improvement, and replacing the chained .replace() matchConfigPattern with the canonical single-pass tokenizer from config-matchers.ts is a real security fix. The new request-pipeline.test.ts file adds meaningful coverage for the extracted functions. Tests pass (33/33 request-pipeline, 209/209 app-router).

I've verified that all imported functions (requestContextFromRequest, matchRedirect, matchRewrite, matchHeaders, sanitizeDestination, isExternalUrl, proxyExternalRequest, parseCookies) exist in config-matchers.ts with compatible signatures, and the safeRegExp removal is clean (no remaining references in index.ts).

Two items that should be addressed:

1. Dangling comment on line 3856 of index.ts

The comment // sanitizeDestination is imported from config-matchers.ts sits alone between the matchConfigPattern re-export and the stripServerExports function. It's a leftover from removing sanitizeDestinationLocal and doesn't correspond to any code — sanitizeDestination is imported at line 37 and used in applyRedirects/applyRewrites further down. This should be removed.

2. Orphaned comment on line 1094 of index.ts (generated code)

The comment // parseCookies is imported from config-matchers.ts (see import statements above) replaces the old inline parseCookies function in the generated entry template. While it's not harmful, leaving a comment in generated runtime code pointing at import statements (that the user can't see in the generated output) is confusing. Either remove the comment entirely or replace it with something minimal like // (imported above).

Minor observations (non-blocking)

  • normalizeTrailingSlash uses relative Location headers where the old code used Response.redirect(new URL(..., request.url)) producing absolute URLs. This is actually an improvement (aligns dev with prod-server.ts, and relative Location is valid per RFC 7231), but it's a behavioral change worth noting in the PR description.

  • processMiddlewareHeaders is clean and correct — the previous bot reviews flagged a now-removed applyTo parameter that no longer exists in the code. The current implementation is just a delete loop, which is exactly what the call site needs.

  • Import path conventions are consistent: app-dev-server.ts uses fileURLToPath(new URL(..., import.meta.url)) (ESM style) while index.ts uses path.resolve(__dirname, ...) (CJS-compat style). Both are established patterns in their respective files.

// divergence that CodeQL flagged as incomplete sanitization).
export { matchConfigPattern } from "./config/config-matchers.js";

// sanitizeDestination is imported from config-matchers.ts
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment is a dangling stub — it doesn't correspond to any code. sanitizeDestination is imported at line 37 and used in applyRedirects/applyRewrites below. This was left behind when sanitizeDestinationLocal was removed. Remove it.

Suggested change
// sanitizeDestination is imported from config-matchers.ts

}
return cookies;
}
// parseCookies is imported from config-matchers.ts (see import statements above)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment in the generated entry template is confusing — it points at "import statements above" that aren't visible in the generated output at runtime. Consider removing it or making it minimal:

Suggested change
// parseCookies is imported from config-matchers.ts (see import statements above)

@ask-bonk
Copy link
Contributor

ask-bonk bot commented Mar 8, 2026

Review submitted successfully. Here's a summary of what I found:

Overall: The PR is solid. The deduplication of ~250 lines of inline __-prefixed helpers into importable modules is a clear improvement, and the matchConfigPattern consolidation (replacing chained .replace() with the canonical single-pass tokenizer) is a genuine security fix. All tests pass (33/33 request-pipeline, 209/209 app-router).

Two items to address:

  1. Dangling comment at index.ts:3856// sanitizeDestination is imported from config-matchers.ts is a leftover stub that doesn't correspond to any code. Should be removed.
  2. Orphaned comment at index.ts:1094 — The comment in the generated entry template points at "import statements above" that aren't visible in generated output. Confusing for anyone reading the generated code.

Non-blocking observations:

  • normalizeTrailingSlash switches from absolute to relative Location headers (actually an improvement, aligns dev with prod)
  • processMiddlewareHeaders is clean — previous bot reviews were flagging an applyTo parameter that no longer exists in the code
  • Import path conventions are consistent with established patterns in each file

github run

@james-elicx
Copy link
Collaborator

/bigbonk please review this pr.

Copy link
Contributor

@ask-bonk ask-bonk bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: Extract shared request handling from server entry points

The direction of this PR is sound. Extracting inline __-prefixed helpers into importable modules is a clear structural improvement, and replacing the chained .replace() matchConfigPattern with the canonical single-pass tokenizer from config-matchers.ts is a real security fix. The new request-pipeline.ts module is well-documented, the function signatures are clean, and the test file adds meaningful coverage for all six exported functions.

What looks good

  • matchConfigPattern re-export from index.ts preserves the public API correctly (line 3852) with a clear comment explaining why.
  • sanitizeDestinationLocalsanitizeDestination swap is clean — the config-matchers version has the same semantics.
  • requestContextFromRequest is a straightforward 1:1 replacement for __buildRequestContext at both call sites (app-dev-server.ts:1197, index.ts:2995).
  • validateCsrfOrigin signature change from closure-captured __allowedOrigins to an explicit parameter is correct and improves testability.
  • safeRegExp removal from the index.ts import is verified clean — zero remaining references.
  • processMiddlewareHeaders is simple and correct — just a delete loop, which is exactly what the call site needs (line 1374, after applyMiddlewareRequestHeaders already handles unpacking).
  • Module header comment (lines 4-7) is accurate after incorporating the earlier review feedback.
  • Test file exercises all six exported functions with good edge case coverage.

Issues to address

See inline comments — two minor items that should be cleaned up.

if (trailingSlash && !hasTrailing && !pathname.endsWith(".rsc")) {
return new Response(null, {
status: 308,
headers: { Location: basePath + pathname + "/" + search },
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking observation: This is a subtle behavioral change from the old code. The old inline version used Response.redirect(new URL(basePath + pathname + "/" + search, request.url), 308), which produces an absolute Location header. This new version produces a relative path.

This is actually an improvement — it aligns dev behavior with prod-server.ts (which already uses relative Location headers), and relative redirects are valid per RFC 7231 §7.1.2. But since it changes what browsers see in the Location header, it's worth noting in the PR description as a deliberate behavioral change.

if (__imgResult instanceof Response) return __imgResult;
// In dev, redirect to the original asset URL so Vite's static serving handles it.
return Response.redirect(__resolvedImg.href, 302);
return Response.redirect(new URL(__imgResult, url.origin).href, 302);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch on simplifying the image validation flow. One thing worth noting: the old code resolved against request.url (full URL with path+query) while the new validateImageUrl resolves against url.origin. For root-relative paths this is equivalent. The defense-in-depth origin check inside validateImageUrl is documented as tautological for current inputs (line 191-194 of request-pipeline.ts), which is the right call.

${middlewarePath ? `import * as middlewareModule from ${JSON.stringify(middlewarePath.replace(/\\/g, "/"))};` : ""}
${instrumentationPath ? `import * as _instrumentation from ${JSON.stringify(instrumentationPath.replace(/\\/g, "/"))};` : ""}
${effectiveMetaRoutes.length > 0 ? `import { sitemapToXml, robotsToText, manifestToJson } from ${JSON.stringify(fileURLToPath(new URL("./metadata-routes.js", import.meta.url)).replace(/\\/g, "/"))};` : ""}
import { requestContextFromRequest, matchRedirect, matchRewrite, matchHeaders, isExternalUrl, proxyExternalRequest, sanitizeDestination } from ${JSON.stringify(configMatchersPath)};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: Both imports are unconditional, but some of the imported functions are only emitted inside conditional template blocks — stripBasePath is gated by ${bp ? ...}, processMiddlewareHeaders is gated by middleware existence, etc. Since these imports appear in the generated virtual RSC entry (not the build output), the tree-shaking concern is negligible for dev-only code. Just noting this is intentional/acceptable.

host: request.headers.get("host") || url.host,
};
}
// ── Config pattern matching, redirects, rewrites, headers, CSRF validation,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a nice replacement for the ~250 lines of inline code. The comment is clear and explains the rationale well. The generateSafeRegExpCode and generateNormalizePathCode remaining inline (lines 1078, 1081) makes sense since they generate function definitions that middleware matching needs at runtime in the virtual entry.

@ask-bonk
Copy link
Contributor

ask-bonk bot commented Mar 8, 2026

Review submitted successfully. Here's a summary of what I found:

Overall assessment: COMMENT (no blocking issues)

The PR is in good shape. The deduplication of ~250 lines of inline __-prefixed helpers into importable modules is a clear structural improvement. The matchConfigPattern consolidation (replacing chained .replace() with the canonical single-pass tokenizer) is a genuine security fix. All function signatures in config-matchers.ts match their call sites correctly.

Key findings:

  1. All imports are verified correct — safeRegExp removal is clean (zero remaining references), parseCookies is properly imported in the generated entry code, and all config-matchers function signatures match their usage.
  2. processMiddlewareHeaders is clean and simple — just a delete loop, which is exactly what the call site needs after applyMiddlewareRequestHeaders handles the unpacking.
  3. Previous bot reviews' concerns about "dead code" were incorrect — the functions are used inside the generated code template literal, not via regular imports.
  4. normalizeTrailingSlash has a subtle behavioral change (absolute → relative Location headers) that's actually an improvement (aligns dev with prod), noted as non-blocking.
  5. The module header comment, test coverage, and code organization are all solid.

github run

@yunus25jmi1
Copy link
Contributor Author

yunus25jmi1 commented Mar 8, 2026

All review items addressed across 3 commits:

  • d15f7d — Removed dead applyTo parameter from processMiddlewareHeaders, simplified to a delete loop. Also removed the dangling sanitizeDestination comment in index.ts.
  • 44139b0 — Merged main to resolve conflicts (kept both the parseCookies import and the new instrumentationImportCode).
  • 48e4924 — Removed the orphaned parseCookies comment from the generated entry template (line 1094) and the second sanitizeDestination stub reintroduced by the merge (line 3856).

The PR description has also been updated to document the normalizeTrailingSlash behavioral change (absolute → relative Location headers). This aligns dev behavior with prod-server.ts and remains valid per RFC 7231.

Latest Bonk review confirmed no blocking issues.
@james-elicx could you dismiss the stale REQUEST_CHANGES review so the merge check can pass?

@yunus25jmi1
Copy link
Contributor Author

@james-elicx review the PR.

@james-elicx
Copy link
Collaborator

/bigbonk please re-review the pr

Copy link
Contributor

@ask-bonk ask-bonk bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: Extract shared request handling from server entry points

This is a well-structured refactoring PR. The deduplication of inline __-prefixed helpers into importable modules is a clear improvement, and replacing the chained .replace() matchConfigPattern with the canonical single-pass tokenizer from config-matchers.ts is a real security fix. The code quality, documentation, and test coverage are solid.

What looks good

  • matchConfigPattern consolidation — Removing the duplicate implementation from index.ts (~120 lines) and re-exporting from config-matchers.ts is the highest-value change. The single-pass tokenizer is correct and eliminates the CodeQL-flagged chained .replace() divergence.
  • requestContextFromRequest — Clean 1:1 replacement for __buildRequestContext at both call sites. The Node.js IncomingMessage → Web Request conversion in index.ts:2985-2996 is correctly documented.
  • validateCsrfOrigin parameterization — Moving from closure-captured __allowedOrigins to an explicit parameter improves testability and makes the dependency explicit.
  • processMiddlewareHeaders is clean — a simple delete loop matching what the call site needs.
  • Module header comment (lines 4-7 of request-pipeline.ts) is accurate and honest about the migration scope.
  • Test coverage — 33 tests covering all 6 exported functions with good edge case coverage (backslash variants, wildcard subdomains, missing parameters, etc.)
  • safeRegExp removal from index.ts imports is verified clean — zero remaining references.

Issues

See inline comments for two items worth addressing.

Comment on lines +1262 to +1263
{
const __tsRedirect = normalizeTrailingSlash(pathname, __basePath, __trailingSlash, url.search);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The old inline code conditionally ran trailing-slash logic:

if (pathname !== "/" && !pathname.startsWith("/api")) {

The new code unconditionally calls normalizeTrailingSlash() which has the same guard internally, so correctness is preserved. However, wrapping the call in a bare block { ... } to scope __tsRedirect is an unusual pattern in generated code. A simpler approach:

Suggested change
{
const __tsRedirect = normalizeTrailingSlash(pathname, __basePath, __trailingSlash, url.search);
const __tsRedirect = normalizeTrailingSlash(pathname, __basePath, __trailingSlash, url.search);
if (__tsRedirect) return __tsRedirect;

The __tsRedirect variable doesn't need scoping — it's immediately checked and the function returns if non-null. The block is harmless but adds visual noise to the generated entry.

if (__basePath && pathname.startsWith(__basePath)) {
pathname = pathname.slice(__basePath.length) || "/";
}
pathname = stripBasePath(pathname, __basePath);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: when bp is truthy, this emits pathname = stripBasePath(pathname, __basePath). The stripBasePath function already handles the case where basePath is empty or the pathname doesn't start with basePath, so the conditional template guard ${bp ? ... : ""} is technically redundant — you could always emit the call. Not suggesting a change here since the conditional avoids a no-op function call in the common case (no basePath), just noting the safety margin.

if (trailingSlash && !hasTrailing && !pathname.endsWith(".rsc")) {
return new Response(null, {
status: 308,
headers: { Location: basePath + pathname + "/" + search },
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the behavioral change documented in the PR description — switching from Response.redirect(new URL(..., request.url)) (absolute Location) to a relative Location header. I've verified that prod-server.ts (lines 769, 774) already uses relative Location headers with the same pattern:

res.writeHead(308, { Location: basePath + pathname + "/" + qs });

So this actually brings dev/prod into alignment. The change is correct and the PR description properly documents it.

import { safeJsonStringify } from "vinext/html";
import { getSSRFontLinks as _getSSRFontLinks, getSSRFontStyles as _getSSRFontStylesGoogle, getSSRFontPreloads as _getSSRFontPreloadsGoogle } from "next/font/google";
import { getSSRFontStyles as _getSSRFontStylesLocal, getSSRFontPreloads as _getSSRFontPreloadsLocal } from "next/font/local";
import { parseCookies } from ${JSON.stringify(path.resolve(__dirname, "config/config-matchers.js").replace(/\\/g, "/"))};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good change. The generated Pages Router entry now imports parseCookies from the canonical config-matchers.ts instead of inlining its own copy. The import uses path.resolve(__dirname, ...) which is consistent with the established CJS-compat pattern in this file (vs. the fileURLToPath(new URL(..., import.meta.url)) ESM pattern in app-dev-server.ts).

it("returns null when pathname already matches the trailingSlash setting", () => {
expect(normalizeTrailingSlash("/about/", "", true, "")).toBeNull();
expect(normalizeTrailingSlash("/about", "", false, "")).toBeNull();
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The trailing slash tests are thorough but missing one edge case: multiple trailing slashes. The regex pathname.replace(/\/+$/, "") in normalizeTrailingSlash handles /about////about, but there's no test for it. Consider adding:

it("strips multiple trailing slashes when trailingSlash is false", () => {
  const res = normalizeTrailingSlash("/about///", "", false, "");
  expect(res).not.toBeNull();
  expect(res!.headers.get("Location")).toBe("/about");
});

});
// When host is missing, the function returns null (allows)
expect(validateCsrfOrigin(req)).toBeNull();
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good coverage of the CSRF validation. One additional edge case worth considering: the Host header with a port vs the Origin without a port (or vice versa). For example:

  • Host: localhost:3000 + Origin: http://localhost (origin omits port, but port 80/443 are implicit)
  • This is correctly handled by new URL(originHeader).host which includes the port when non-default.

Not blocking — just noting the test doesn't exercise port mismatch scenarios, which are covered by the host property behavior of the URL constructor.

@ask-bonk
Copy link
Contributor

ask-bonk bot commented Mar 8, 2026

Review submitted successfully. Here's a summary of my findings:

Overall: This is a solid refactoring PR. The direction is right — deduplicating ~250 lines of inline __-prefixed helpers into shared modules is a real structural improvement, and the matchConfigPattern consolidation (single-pass tokenizer replacing chained .replace()) is a genuine security fix.

Key verified items:

  • All imports from config-matchers.ts and request-pipeline.ts have correct signatures and matching call sites
  • safeRegExp removal is clean (zero remaining references in index.ts)
  • The normalizeTrailingSlash behavioral change (absolute → relative Location headers) is correctly documented in the PR description and aligns dev behavior with prod-server.ts
  • Previous bot reviews' dangling comment concerns have been addressed — no stale sanitizeDestination or parseCookies comments remain
  • processMiddlewareHeaders is correctly simplified to just a delete loop
  • Test coverage is comprehensive (33 tests for all 6 exported functions)

Inline feedback posted:

  1. The bare block { ... } scoping __tsRedirect in generated code is unnecessary visual noise — suggested simplifying
  2. Suggested adding a test for multiple trailing slashes (/about///)
  3. Confirmed the normalizeTrailingSlash behavioral change is correct with prod-server verification
  4. Noted the parseCookies import in index.ts follows established conventions
  5. Noted port mismatch scenarios in CSRF tests as a non-blocking observation

github run

@yunus25jmi1
Copy link
Contributor Author

Addressed the two actionable items from the latest bonk review:

  1. Removed bare block around __tsRedirect — The { const __tsRedirect = ... } scoping block was unnecessary since the variable is immediately checked and the function returns if non-null. Simplified to a flat const __tsRedirect = ... (app-dev-server.ts:1262-1263).

  2. Added multiple trailing slashes test — New test case verifying normalizeTrailingSlash("/about///", "", false, "") correctly redirects to /about (request-pipeline.test.ts).

All 243 tests pass (209 app-router + 34 request-pipeline).

@yunus25jmi1
Copy link
Contributor Author

Review the PR and merge it. @james-elicx

@james-elicx
Copy link
Collaborator

/bigbonk re-review the pr please

Copy link
Contributor

@ask-bonk ask-bonk bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review

Solid refactoring. The core value here is real: consolidating the chained .replace() matchConfigPattern onto the canonical single-pass tokenizer from config-matchers.ts, and replacing the ~250 lines of inline __-prefixed helpers with imports. The new request-pipeline.ts module is well-documented, the function signatures are clean, and the test coverage (34 tests) is meaningful.

I verified:

  • All imports from config-matchers.ts and request-pipeline.ts have matching exports with compatible signatures
  • safeRegExp removal from index.ts imports is clean (zero remaining references)
  • parseCookies in the generated Pages Router entry correctly imports from the canonical config-matchers.ts and the call site at line 1106 matches the old inline usage
  • The normalizeTrailingSlash behavioral change (absolute → relative Location) aligns dev with prod-server.ts (lines 769, 774 use relative paths)
  • Previous reviewers' dangling comment concerns have been addressed (no stale sanitizeDestination or parseCookies comments remain)
  • processMiddlewareHeaders is correctly simplified to a delete loop matching the call site need

Two minor items inline — neither is blocking.

export function guardProtocolRelativeUrl(rawPathname: string): Response | null {
// Normalize backslashes: browsers and the URL constructor treat
// /\evil.com as protocol-relative (//evil.com), bypassing the // check.
if (rawPathname.replaceAll("\\", "/").startsWith("//")) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: String.prototype.replaceAll is available in Node 15+ and all modern browsers, so this is fine for runtime. But the old inline code in the generated entry used double-backslash "\\\\" (escaped for the template literal context), while this version uses a single "\\". Both are correct — the old code was inside a template literal where \\ becomes \ at runtime, and this code is a regular .ts file where "\\" is already \. Just noting for anyone confused reading the diff.

trailingSlash: boolean,
search: string,
): Response | null {
if (pathname === "/" || pathname.startsWith("/api")) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor edge case worth considering: paths like /api-docs/ will not be redirected because pathname.startsWith("/api") matches them. This matches the old inline behavior exactly (same guard), so it's not a regression. But Next.js actually checks for /api/ or exactly /api — the prefix match here is slightly too broad.

Not something to fix in this PR since it's a pre-existing behavioral difference, but worth filing as a follow-up issue.

@ask-bonk
Copy link
Contributor

ask-bonk bot commented Mar 8, 2026

Review submitted. Here's a summary:

Overall assessment: COMMENT (no blocking issues)

The PR is in good shape. The core improvements are:

  1. matchConfigPattern consolidation — Replacing the chained .replace() implementation with the canonical single-pass tokenizer from config-matchers.ts is a genuine security fix (CodeQL-flagged divergence eliminated).

  2. ~250 lines of inline __-prefixed helpers removed from app-dev-server.ts and replaced with imports from config-matchers.ts and the new request-pipeline.ts.

  3. matchConfigPattern re-export from index.ts preserves the public API while removing the duplicate implementation (~120 lines including extractConstraint).

  4. Test coverage — 34 unit tests for the 6 exported functions in request-pipeline.ts, plus updated app-router test assertions.

I posted two non-blocking inline comments:

  • A note about the replaceAll("\\", "/") escaping difference between the old template literal context and the new .ts file (both correct, just potentially confusing in the diff).
  • A pre-existing edge case where /api-docs/ paths skip trailing-slash normalization due to the .startsWith("/api") guard being slightly too broad vs Next.js's actual behavior — not a regression, but worth a follow-up issue.

github run

@yunus25jmi1
Copy link
Contributor Author

yunus25jmi1 commented Mar 8, 2026

Latest Bonk review confirmed no blocking issues — both inline comments are non-blocking observations:

  1. replaceAll escaping note — informational; both the old and new code are correct.
  2. /api prefix edge case — a pre-existing behavioral difference (not a regression). Worth tracking in a follow-up issue.

The stale REQUEST_CHANGES from a prior Bonk review is the only thing blocking the merge.

@james-elicx could you dismiss it?

@james-elicx
Copy link
Collaborator

i was going to reply to bonk's comment on /api that it would probably be worth addressing in this one.

i can take care of it in the morning if you want

…ches only /api or /api/), add tests for edge cases
@yunus25jmi1
Copy link
Contributor Author

Addressed the /api prefix edge case:

  • All trailing slash normalization now matches only /api or /api/ (not /api-docs etc), aligning with Next.js behavior.
  • Updated all 4 locations: request-pipeline.ts, deploy.ts, index.ts, prod-server.ts.
  • Added tests for /api, /api/, and /api-docs edge cases to request-pipeline.test.ts.
  • All 37 request-pipeline tests pass. App-router tests pass. Pages-router has 2 pre-existing failures unrelated to this change.

Ready for merge. Please dismiss stale REQUEST_CHANGES.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Refactor: Extract shared request handling logic from the four server entry points

2 participants